/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package filecopier.gui.copy;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Calendar;
import java.util.SimpleTimeZone;
import java.beans.PropertyChangeSupport;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.SwingWorker;
import javax.swing.JTextArea;
import javax.swing.JOptionPane;
import javax.swing.JFileChooser;
import filecopier.Singleton;
import filecopier.ActionCommand;
import filecopier.ActionCancelException;
import filecopier.options.Options;
import filecopier.options.RenameFmtString;
import filecopier.gui.ProgressBarPanel;
import filecopier.copy.AbstractCopyWorker;
import filecopier.gui.WorkerMessage;
import filecopier.script.Script;
import filecopier.script.RestoreScript;
import filecopier.script.ScriptEntry;
import filecopier.script.RestoreScriptEntry;
import custom.xml.DOMHelperException;
import custom.util.FileCopy;
import custom.util.FileCopyHelper;
import custom.util.FileCopyCancelException;
import org.apache.log4j.Logger;
import org.apache.log4j.Level;

/**
 *
 * @author david
 */
public class CopyGuiWorker extends AbstractCopyWorker implements ActionCommand, PropertyChangeListener {

    private PropertyChangeSupport pcs = null;
    private ProgressBarPanel progressBarPanel = null;
    private File newNameFile = null;

    public CopyGuiWorker(final Script fromScript, final Script toScript) {
        super(fromScript, toScript);
        pcs = new PropertyChangeSupport(this);
    }

    public PropertyChangeSupport getPropertyChangeSupport() {
        return pcs;
    }

    public void doAction() throws ActionCancelException {
        copyFiles();
    }

    public void copyFiles() throws ActionCancelException {
        progressBarPanel = new ProgressBarPanel("Copying files...");
        new CopySwingWorker().execute();
    }

    public void propertyChange(PropertyChangeEvent pce) {
        if ("confirmOverwritePrompt".equals(pce.getPropertyName())) {
            confirmOverwrite((File) pce.getNewValue());
        } else if ("newNamePrompt".equals(pce.getPropertyName())) {
            promptForNewName((File) pce.getNewValue());
        }
    }

    private void promptForNewName(final File newNameFile) {
        // Should not have to create the file like this...
        try {
            if (!newNameFile.exists()) {
                newNameFile.createNewFile();
            }
        } catch (IOException e) {
        }
        Singleton.getInstance().getFileAlwaysLogger().info("Prompting user for new name");
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setApproveButtonMnemonic('R');
        fileChooser.setCurrentDirectory(new File(newNameFile.getParent()));
        fileChooser.setSelectedFile(newNameFile);

        int rename = fileChooser.showDialog(Singleton.getInstance().getFrame(), "Rename");
        if (JFileChooser.APPROVE_OPTION == rename) {
            // If value is APPROVE_OPTION, the user picked a file name so set this.newNameFile to that.
            this.newNameFile = fileChooser.getSelectedFile();
        } else {
            // Otherwise, the user did not select a file, so set this.newNameFile to null.
            this.newNameFile = null;
        }
        pcs.firePropertyChange("newNameValue", null, rename);
    }

    private void confirmOverwrite(final File existingFile) {
        Object[] possibleValues = {"Yes", "Yes To All", "No", "No To All"};
        int val = JOptionPane.showOptionDialog(Singleton.getInstance().getFrame(),
                existingFile + " already exists. Are you sure you want to overwrite?",
                "Confirm Overwrite", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
                possibleValues, possibleValues[0]);

        // Send the user's response to the CopySwingWorker thread.
        pcs.firePropertyChange("confirmOverwriteValue", null, val);
    }

    private class CopySwingWorker extends SwingWorker<Void, WorkerMessage> implements PropertyChangeListener, FileCopyHelper {

        private int fileCt = 0;
        private int attemptCt = 0;
        private float oneFilePercent = 0;
        private Logger logger = null;
        private Script restoreScript = null;
        private File restoreScriptFile = null;
        // The member variables below are for CopySwingWorker's implementation of FileCopyHelper
        private boolean update = false;
        private boolean confirmOverwrite = false;
        private boolean useGMT = false;
        private RenameFmtString renameFmtString = null;
        private Options.RenameType renameType = null;
        private int restoreCount = 0;
        private int copiedCt = 0;
        private int confirmOverwriteValue = PROMPT_INIT_VALUE;
        private volatile int newNameValue = PROMPT_INIT_VALUE;
        private volatile Boolean cancelRequest = false;
        private static final int PROMPT_INIT_VALUE = -2;
        private static final int YES_OPTION = 0;
        private static final int YESTOALL_OPTION = 1;
        private static final int NO_OPTION = 2;
        private static final int NOTOALL_OPTION = 3;

        public CopySwingWorker() {
            logger = getLogger(progressBarPanel.getTextArea());
            init();
        }

        public void init() {
            Options options = Singleton.getInstance().getOptions();
            useGMT = options.getUseGMT();
            update = options.getUpdateOnly();
            renameType = options.getRenameType();
            confirmOverwrite = options.promptForOverwrite();
            renameFmtString = options.getRenameFmtString();
            logger = Singleton.getInstance().getFileGuiLogger();
            initRestoreScript();
        }

        private void initRestoreScript() {
            if (Singleton.getInstance().getOptions().buildRestoreScript()) {
                restoreScriptFile = buildDefaultRestoreScriptFile(fromScript, toScript);
                try {
                    restoreScript = new RestoreScript();
                } catch (DOMHelperException e) {
                    // Log the exception, notify user that a restore script will not be built, and set restoreScript to null.
                    Singleton.getInstance().getFileLogger().error("The restore script \"" + restoreScriptFile + "\" cannot be built", e);
                    Singleton.getInstance().getGuiLogger().error("The restore script \"" + restoreScriptFile + "\" cannot be built:" + e.getMessage());
                    restoreScript = null;
                }
            }
        }

        @Override
        protected Void doInBackground() throws ActionCancelException {
            List<File> fromFiles = getFiles(fromScript);
            List<File> toFiles = getFiles(toScript);
            fileCt = countFiles(fromFiles, toFiles, Singleton.getInstance().getOptions());
            oneFilePercent = (float) 100 / fileCt;

            // Add all of the property change listeners.
            addPropertyChangeListeners();
            addCopyGuiWorkerPropertyChangeListeners();
            addProgressBarPanelPropertyChangeListeners();
            addCopySwingWorkerChangeListeners();

            int copyCt = 0;
            try {
                copyCt = getFileCopy(this).copy(fromFiles, toFiles);
            } catch (FileCopyCancelException e) {
                handleRestoreScript();
                throw new ActionCancelException(e);
            }
            
            handleRestoreScript();
            if (copyCt == fileCt) {
                if (fromScript.isTemp() && fromScript.getScriptFile().delete()) {
                    publish(new WorkerMessage(logger, Level.INFO, "Temporary copy script file \"" +
                            fromScript.getScriptName() + "\" was deleted."));
                    setTempScriptDeleted(fromScript.getScriptName());
                }
                if (toScript.isTemp() && toScript.getScriptFile().delete()) {
                    publish(new WorkerMessage(logger, Level.INFO, "Temporary copy script file \"" +
                            toScript.getScriptName() + "\" was deleted."));
                    setTempScriptDeleted(toScript.getScriptName());
                }
            }
            
            return null;
        }
        
        private void handleRestoreScript() {
            if (restoreScript != null) {
                if (this.getRestoreCount() > 0) {
                    try {
                        publish(new WorkerMessage(Singleton.getInstance().getGuiLogger(), Level.INFO,
                            "Saving restore script..."));
                        restoreScript.saveToDisk(restoreScriptFile);
                        publish(new WorkerMessage(logger, Level.INFO, "Files that were renamed can be restored by " +
                                "running the restore script \"" + restoreScript.getScriptName() + "\"."));
                        setRestoreScriptCreated(restoreScriptFile);
                    } catch (DOMHelperException e) {
                        Singleton s = Singleton.getInstance();
                        WorkerMessage guiMessage = new WorkerMessage(s.getGuiLogger(), Level.ERROR,
                                "The restore script + \"" + restoreScript.getScriptName() + "\" could not be saved");
                        WorkerMessage fileMessage = new WorkerMessage(s.getFileLogger(), Level.ERROR,
                                "The restore script + \"" + restoreScript.getScriptName() + "\" could not be saved", e);
                        publish(guiMessage, fileMessage);
                    } catch (IOException e) {
                        Singleton s = Singleton.getInstance();
                        WorkerMessage guiMessage = new WorkerMessage(s.getGuiLogger(), Level.ERROR,
                                "The restore script + \"" + restoreScript.getScriptName() + "\" could not be saved");
                        WorkerMessage fileMessage = new WorkerMessage(s.getFileLogger(), Level.ERROR,
                                "The restore script + \"" + restoreScript.getScriptName() + "\" could not be saved", e);
                        publish(guiMessage, fileMessage);
                    }
                } else {
                    File restoreFile = restoreScript.getScriptFile();
                    if (restoreFile != null) {
                        restoreFile.delete();
                    }
                }
            }
        }

        @Override
        protected void process(List<WorkerMessage> chunks) {
            for (WorkerMessage wm : chunks) {
                if (wm.getException() == null) {
                    wm.getLogger().log(wm.getLevel(), wm.getMessage());
                } else {
                    wm.getLogger().log(wm.getLevel(), wm.getMessage(), wm.getException());
                }
            }
        }

        @Override
        public void done() {
            firePropertyChange("summaryRpt", null, copiedCt + " out of " + fileCt + " files successfully copied.");
        }

        public void propertyChange(PropertyChangeEvent pce) {
            String propertyName = pce.getPropertyName();
            if (propertyName.equals("confirmOverwriteValue")) {
                confirmOverwriteValue = (Integer) pce.getNewValue();
            } else if (propertyName.equals("newNameValue")) {
                newNameValue = (Integer) pce.getNewValue();
            } else if(propertyName.equals("cancel")) {
                cancelRequest = true;
            }
        }

        private void addPropertyChangeListeners() {
            PropertyChangeSupport pcs = getPropertyChangeSupport();
            // Add the CopyGuiWorker as a listener for "confirmOverwritePrompt. When this property change is received,
            // CopyGuiWorker creates a JOptionPane that asks the user to confirm overwriting a file.
            pcs.addPropertyChangeListener("confirmOverwritePrompt", CopyGuiWorker.this);
            // Add the CopyGuiWorker as a listener for "newNamePrompt. When this property change is received,
            // CopyGuiWorker creates a JFileChooser that asks the user for a new name to a file that already exists.
            pcs.addPropertyChangeListener("newNamePrompt", CopyGuiWorker.this);
        }

        private void addCopyGuiWorkerPropertyChangeListeners() {
            PropertyChangeSupport pcs = CopyGuiWorker.this.getPropertyChangeSupport();
            // Add the FileCopyGuiHelper as a listener for "confirmOverwriteValue". This is the user's response to the
            // prompt to confirm overwriting a file.
            pcs.addPropertyChangeListener("confirmOverwriteValue", this);
            // Add the FileCopyGuiHelper as a listener for "newNameValue". This is the user's response to the
            // prompt to confirm overwriting a file.
            pcs.addPropertyChangeListener("newNameValue", this);
        }

        private void addProgressBarPanelPropertyChangeListeners() {
            // Now, add the copyWorker's FileCopyHelper as a PropertyChangeListener to the ProgressBarPanel. It needs to listen
            // for if the Cancel Button is pressed.
            progressBarPanel.addPropertyChangeListener("cancel", this);
        }

        private void addCopySwingWorkerChangeListeners() {
            // Call getPropertyChangeSupport so I can specify the exact property I want the listeners to listen for. The SwingWorker
            // does not expose the addPropertyChangeListner(String propertyName, PropertyChangeListener pcl) method.
            this.getPropertyChangeSupport().addPropertyChangeListener("progress", progressBarPanel);
            this.getPropertyChangeSupport().addPropertyChangeListener("state", progressBarPanel);
            this.getPropertyChangeSupport().addPropertyChangeListener("summaryRpt", progressBarPanel);
        }

        private Logger getLogger(JTextArea textArea) {
            logger = Singleton.getInstance().getGuiLogger();

            org.apache.log4j.Appender textAreaAppender = logger.getAppender("JTextAreaAppender");

            if (textAreaAppender != null && textAreaAppender instanceof custom.log4j.JTextAreaAppender) {
                ((custom.log4j.JTextAreaAppender) textAreaAppender).setTextArea(textArea);
            }
            return logger;
        }

        private void updateProgress() {
            float progressFloat = (float) attemptCt * oneFilePercent;
            int progress = (int) progressFloat;
            // Set the progress property. This will update the progress bar.
            setProgress(attemptCt < fileCt ? Math.min(progress, 100) : Math.max(progress, 100));
        }

        //***********************************************************************************
        //The methods below are for the CopySwingWorker's implementation of FileCopyHelper  *
        //***********************************************************************************
        public void handleException(final IOException e) {
            // Send the error message only to the gui logger and the error message plus the stack trace to the file logger.
            Singleton s = Singleton.getInstance();
            WorkerMessage guiMessage = new WorkerMessage(s.getGuiLogger(), Level.ERROR, e.getMessage());
            WorkerMessage fileMessage = new WorkerMessage(s.getFileLogger(), Level.ERROR, "IOException", e);
            publish(guiMessage, fileMessage);

            ++attemptCt;
            updateProgress();
        }

        public boolean handleExistingFile(final File existingFile, final File sourceFile) throws FileCopyCancelException {
            boolean status = true;

            /* If update is true, we are supposed to make sure that the destination file is older than the source file
            before copying it. If the files are directories, however, I do not want to perform this check. The check
            will be performed for each file in the directory. */
            if ((update == true) && (!existingFile.isDirectory() && !sourceFile.isDirectory()) &&
                    (existingFile.lastModified() >= sourceFile.lastModified())) {
                publish(new WorkerMessage(logger, Level.INFO, "\"" + existingFile + "\" is not older than " + sourceFile + "\". Skipping file."));
                status = false;
            } else {
                if (renameType == Options.RenameType.Skip) {
                    // Rename type is skip, which means do not copy any existing files.
                    publish(new WorkerMessage(logger, Level.INFO, "\"" + existingFile + "\" already exists. Skipping file."));
                    status = false;
                } else if (renameType == Options.RenameType.Overwrite) {
                    status = handleOverwrite(existingFile);
                } else if (renameType == Options.RenameType.RenameAuto) {
                    status = handleRenameAuto(existingFile);
                } else {
                    status = handleRenameManual(existingFile);
                }
            }

            return status;
        }

        public boolean handleFileInUse(final File existingFile) {
            boolean status = false;
            if (renameType == Options.RenameType.Overwrite) {
                publish(new WorkerMessage(logger, Level.INFO, "Overwrite failed because \"" + existingFile + "\" is in use by another process. Attempting rename."));
                status = handleRenameAuto(existingFile);
            }

            return status;
        }

        public void reportCopyStatus(final String msg, boolean status) throws FileCopyCancelException {
            if (status) {
                publish(new WorkerMessage(logger, Level.INFO, msg));
                ++copiedCt;
            } else {
                publish(new WorkerMessage(logger, Level.WARN, msg));
            }

            ++attemptCt;
            updateProgress();

            if (cancelRequest == true) {
                throw new FileCopyCancelException("Copy Operations cancelled.");
            }
        }

        public int getRestoreCount() {
            return restoreCount;
        }

        private boolean handleOverwrite(final File existingFile) throws FileCopyCancelException {
            boolean status = true;

            if (confirmOverwrite) {
                // We have to confirm from the user before doing an overwrite, so send a property change notification to the Event Dispatch Thread.
                setConfirmOverwritePrompt(existingFile);
                // Loop until user responds to the prompt.
                while (confirmOverwriteValue == PROMPT_INIT_VALUE) {
                    // Sleep for 250 milliseconds.
                    try {
                        Thread.sleep(250);
                    } catch (InterruptedException ignore) {
                    }
                }
                // Once we get here, user has responded, so process the response.

                int confirmStatus = confirmOverwriteValue;
                // Reset confirmOverwriteValue to its initial value.
                confirmOverwriteValue = PROMPT_INIT_VALUE;

                // Once we get here, user has responded, so process the response.
                if (confirmStatus == YES_OPTION) {
                    status = true;
                } else if (confirmStatus == YESTOALL_OPTION) {
                    // User said yes to all, so don't confirm subsequent overwrites.
                    confirmOverwrite = false;
                    status = true;
                } else if (confirmStatus == NO_OPTION || confirmStatus == JOptionPane.CLOSED_OPTION) {
                    status = false;
                } else // User selected No To All
                {
                    throw new FileCopyCancelException("Subsequent copy operations aborted");
                }
            }

            return status;
        }

        private boolean handleRenameAuto(final File existingFile) {
            boolean status = true;

            try {
                createNewFileWithLastModified(existingFile);
            } catch (IOException e) {
                status = false;
                handleException(e);
            }

            return status;
        }

        private boolean handleRenameManual(final File existingFile) throws FileCopyCancelException {
            boolean status = true;
            try {
                status = promptForNewName(existingFile);
            } catch (IOException e) {
                status = false;
                handleException(e);
            }

            if (status == false) {
                publish(new WorkerMessage(logger, Level.ERROR, "Renaming of \"" + existingFile + "\" failed."));
                Object[] answer = {"Yes", "No"};
                int result = JOptionPane.showOptionDialog(Singleton.getInstance().getFrame(),
                        "Renaming of \"" + existingFile + "\" failed." +
                        "Do you want to abort all subsequent copy operations?",
                        "Rename Aborted", JOptionPane.YES_NO_OPTION,
                        JOptionPane.QUESTION_MESSAGE, null, answer, answer[0]);

                if (result == JOptionPane.YES_OPTION) {
                    throw new FileCopyCancelException("Subsequent copy operations aborted");
                }
            }

            return status;
        }

        private boolean promptForNewName(final File toFile) throws IOException {
            boolean status = false;
            File newToFile = new File(toFile.getParent(), createNewName(toFile.getName(), toFile.lastModified()));
            int idx = 1;
            while (newToFile.exists() && idx <= Integer.MAX_VALUE) {
                String newName = newToFile.getName();
                newName = incrementNewFileName(newName, idx++);
                newToFile = new File(newToFile.getParent(), newName);
            }

            setNewNamePrompt(newToFile);

            // Loop until user responds to the prompt.
            while (newNameValue == PROMPT_INIT_VALUE) {
                // Sleep for 250 milliseconds.
                try {
                    Thread.sleep(250);
                } catch (InterruptedException ignore) {
                }
            }

            int newNameStatus = newNameValue;
            newNameValue = PROMPT_INIT_VALUE;

            // If newNameStatus is JFileChooser.APPROVE_OPTION, user chose a new name for the file.
            if (newNameStatus == JFileChooser.APPROVE_OPTION) {
                if (!toFile.renameTo(newNameFile)) {
                    throw new IOException("File NOT renamed from \"" + toFile + "\" to \"" + newNameFile + "\"");
                }
                publish(new WorkerMessage(logger, Level.INFO, "File renamed from \"" + toFile + "\" to \"" + newNameFile + "\""));
                status = true;
            }

            // Write info to the restore script file.
            if (restoreScript != null) {
                ScriptEntry rse = new RestoreScriptEntry(newToFile, toFile);
                restoreScript.addScriptEntry(rse);
                restoreCount++;
            }

            return status;
        }

        private void createNewFileWithLastModified(final File toFile) throws IOException {
            File newToFile = new File(toFile.getParent(), createNewName(toFile.getName(), toFile.lastModified()));
            int idx = 1;
            while (newToFile.exists()) {
                String newName = newToFile.getName();
                newName = incrementNewFileName(newName, idx++);
                newToFile = new File(newToFile.getParent(), newName);
            }

            if (!toFile.renameTo(newToFile)) {
                throw new IOException("File NOT renamed from \"" +
                        toFile + "\" to \"" + newToFile + "\"");
            }
            publish(new WorkerMessage(logger, Level.INFO, "File renamed from \"" + toFile + "\" to \"" + newToFile + "\""));
            //options.reportStatus(Level.FINE, "[SUCCESS]" + toFile + " renamed to " + newToFile);
            // Write info to the restore script file.
            if (restoreScript != null) {
                ScriptEntry rse = new RestoreScriptEntry(newToFile, toFile);
                restoreScript.addScriptEntry(rse);
                restoreCount++;
            }
        }

        private String createNewName(final String fileName, final long lastModMillis) {
            String nameOnly, newFileName, ext;
            // See if this file has an extension
            int idx = fileName.lastIndexOf('.');
            if (idx > 0) {
                // File has an extension, so put the name and extension in separate Strings.
                nameOnly = fileName.substring(0, idx);
                ext = fileName.substring(idx);
            } else {
                // File has no extension, so set sNameOnly to point to the whole name and set sExt to point to
                // an empty String.
                nameOnly = fileName;
                ext = "";
            }

            // Get the date that the to file was last modified and put that value in a Calendar object.
            Calendar cal = null;
            if (useGMT) {
                cal = Calendar.getInstance(new SimpleTimeZone(0, "GMT"));
            } else {
                cal = Calendar.getInstance();
            }
            cal.setTimeInMillis(lastModMillis);
            renameFmtString.validateRenameFmtString(cal);

            // Build the new file name with the formatted last modified date string appended to it.
            newFileName = nameOnly + renameFmtString.getPreviewRenameFmtString() + ext;

            return newFileName;
        }

        private String incrementNewFileName(String name, final int idx) {
            String nameOnly, ext;
            // See if this file has an extension
            int extIdx = name.lastIndexOf('.');
            if (extIdx > 0) {
                // File has an extension, so put the name and extension in separate Strings.
                nameOnly = name.substring(0, extIdx);
                ext = name.substring(extIdx);
            } else {
                // File has no extension, so set sNameOnly to point to the whole name and set sExt to point to
                // an empty String.
                nameOnly = name;
                ext = "";
            }

            java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("_\\d+_" + ext + "$");
            java.util.regex.Matcher matcher = pattern.matcher(name);
            int start = -1;
            int end = -1;
            while (matcher.find()) {
                start = matcher.start();
                end = matcher.end();
            }
            StringBuilder sb = new StringBuilder();

            if (start > -1) {
                sb.append(name.substring(0, start)).append("_" + idx + "_").append(ext);
            } else {
                sb.append(nameOnly).append("_" + idx + "_").append(ext);
            }

            return sb.toString();
        }

        private void setConfirmOverwritePrompt(final File existingFile) {
            firePropertyChange("confirmOverwritePrompt", null, existingFile);
        }

        private void setNewNamePrompt(final File newName) {
            firePropertyChange("newNamePrompt", null, newName);
        }

        // The restoreScriptCreated property gets sent by the CopyGuiWorker, not the CopySwingWorker
        private void setRestoreScriptCreated(final File restoreScriptFile) {
            pcs.firePropertyChange("restoreScriptCreated", null, restoreScriptFile);
        }

        // The tempScriptDeleted property gets sent by the CopyGuiWorker, not the CopySwingWorker
        private void setTempScriptDeleted(final String tempScriptName) {
            pcs.firePropertyChange("tempScriptDeleted", null, tempScriptName);
        }
    }
}
